package com.example.emos.wx.user.filter;


import cn.hutool.core.util.StrUtil;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.example.emos.wx.user.shiro.JwtUtil;
import com.example.emos.wx.user.shiro.OAuth2Token;
import com.example.emos.wx.user.shiro.ThreadLocalToken;
import org.apache.http.HttpStatus;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Scope;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMethod;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.concurrent.TimeUnit;

/**
 * 认证过滤器
 * 拦截所有Http请求，一方面会把请求中的Token字符串提取出来封装成对象交给Shiro框架。
 * 另一方面它会检查Token的有效性，如果Token过期，这里指的过期是客户端传递过来的数据是过期的，但是在Redis中存储的数据并没有过期，那么会生成新的Token，否则就要求用户重新登录。
 *
 * @Author YinXi
 * @Versin 1.0.0
 * @Date 2023/7/28
 */
@Component
@Scope("prototype")
public class OAuth2Filter extends AuthenticatingFilter {
    /**
     * 媒介类获取ThreadLocal中保存的变量
     */
    @Autowired
    private ThreadLocalToken threadLocalToken;
    /**
     * 获取缓存过期时间
     */
    @Value("${emos.jwt.cache-expire}")
    private int cacheExpire;
    /**
     * JWT工具类,做校验
     */
    @Autowired
    private JwtUtil jwtUtil;
    /**
     * 操做Redis
     */
    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 拦截请求之后用于把令牌字符串封装成令牌对象
     *
     * @param request  获取http请求
     * @param response 通过http请求进行响应
     * @return AuthenticationToken
     * @throws Exception
     */
    @Override
    protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {
        //强制转换为HttpServletRequest
        HttpServletRequest req = (HttpServletRequest) request;
        //从HttpServletRequest获取对应的token
        String token = getRequestToken(req);
        //如果获取的token是空返回null
        if (StrUtil.isBlank(token)) {
            return null;
        }
        //使用OAuth2Token进行封装为令牌对象后返回
        return new OAuth2Token(token);
    }

    /**
     * 判断哪一种请求可以被Shiro框架处理也就是除了Optional请求之外的其他请求
     *
     * @param request
     * @param response
     * @param mappedValue
     * @return
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        //强制转化为HttpServletRequest
        HttpServletRequest req = (HttpServletRequest) request;
        //判断HttpServletRequest中获取的请求方式是否为OPTIONS
        if (req.getMethod().equals(RequestMethod.OPTIONS.name())) {
            return true;
        }
        return false;
    }

    /**
     * 在isAccessAllowed判断那些请求需要被处理时此处开始工作
     * 主要功能：
     * 从请求头获取令牌字符串
     * 判断令牌字符串是否过期
     * 如果过期了要刷新令牌字符串
     * 新的令牌字符串首先要保存到Reids中，然后保存到媒介类中
     *
     * @return
     * @throws Exception
     */
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        //强制转化为HttpServletRequest和HttpServletReponse
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse rep = (HttpServletResponse) response;
        //设置响应时的字符集和响应的文件类型
        rep.setCharacterEncoding("UTF-8");
        rep.setContentType("text/html");
        //设置允许跨域的参数
        rep.setHeader("Access-Control-Allow-Credentials", "true");
        rep.setHeader("Access-Control-Allow-Origin", req.getHeader("Origin"));
        //清空ThreadLocal，一旦令牌判定为需要刷新，那么令牌不仅仅要保存到Redis还要保存到媒介类中
        threadLocalToken.clear();

        //从请求中获取token
        String token = getRequestToken(req);
        //判断获取的token是否为空
        if (StrUtil.isBlank(token)) {
            //设置响应的状态为401也就是无认证
            rep.setStatus(HttpStatus.SC_UNAUTHORIZED);
            //设置响应的文字
            rep.getWriter().print("无效的令牌");
            //返回错误
            return false;
        }
        //token不为空的情况下对token中的内容进行判断
        //判断内容是否有问题还是时间是否过期
        try {
            //验证有问题时会抛出异常，根据抛出的异常来判断是内容有问题还是时间过期
            jwtUtil.verifierToken(token);

        } catch (TokenExpiredException e) {
            //判断jwt的时间是否过期
            //判断Redis中的令牌是否过期
            //如果有说明客户端的令牌过期了但是服务端的令牌没有过期
            if (redisTemplate.hasKey(token)) {
                //把之前旧的token令牌删除
                redisTemplate.delete(token);
                //根据token解析获取得到其userId
                int userId = jwtUtil.getUserIdFromJwt(token);
                //根据获取得到的userId生成一个token
                token = jwtUtil.createToken(userId);
                //将新生成的token存到redis中
                redisTemplate.opsForValue().set(token, userId + "", cacheExpire, TimeUnit.DAYS);
                //将新生成的token放置到媒介类threadLocalToken中
                threadLocalToken.setToken(token);
            } else {
                //设置响应的状态为401也就是无认证
                rep.setStatus(HttpStatus.SC_UNAUTHORIZED);
                //设置响应的文字
                rep.getWriter().print("令牌过期");
                //返回错误
                return false;
            }
        } catch (Exception e) {
            //判断jwt的内容是否异常
            //设置响应的状态为401也就是无认证
            rep.setStatus(HttpStatus.SC_UNAUTHORIZED);
            //设置响应的文字
            rep.getWriter().print("无效的令牌");
            //返回错误
            return false;
        }
        //token判断合法后执行Realm类
        //间接让Shiro执行Realm类,看返回值为成功还是失败
        boolean realm = executeLogin(request, response);
        return realm;
    }

    /**
     * 在认证执行失败的时候会触发此方法的执行，上述是登录失败的时候这样消息更针对
     *
     * @param token
     * @param e
     * @param request
     * @param response
     * @return
     */
    @Override
    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
        //强制转化为HttpServletRequest和HttpServletReponse
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse rep = (HttpServletResponse) response;
        //设置响应时的字符集和响应的文件类型
        rep.setCharacterEncoding("UTF-8");
        rep.setContentType("text/html");
        //设置允许跨域的参数
        rep.setHeader("Access-Control-Allow-Credentials", "true");
        rep.setHeader("Access-Control-Allow-Origin", req.getHeader("Origin"));
        //设置响应的状态为401也就是无认证
        rep.setStatus(HttpStatus.SC_UNAUTHORIZED);
        //设置响应的文字
        try {
            rep.getWriter().print(e.getMessage());
        } catch (Exception exception) {
        }
        //返回错误
        return false;
    }

    /**
     * 掌管拦截请求和响应的方法和doFilter的作用一样
     * @param request
     * @param response
     * @param chain
     * @throws ServletException
     * @throws IOException
     */
    @Override
    public void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
        super.doFilterInternal(request, response, chain);
    }

    /**
     * 获取请求中的Token
     *
     * @param request 请求
     * @return token字符串
     */
    private String getRequestToken(HttpServletRequest request) {
        //从请求头中获取到Token
        String token = request.getHeader("token");
        //如果从请求头中获取的token是空的话
        if (StrUtil.isBlank(token)) {
            //从请求体中获取数据
            token = request.getParameter("token");
        }
        return token;
    }
}
